﻿// --------------------------------------------------------------------------------------------------------------------
// <copyright file="RequestExtensions.cs" company="Microsoft Corporation">
//   Copyright (c) Microsoft Corporation.  All rights reserved
// </copyright>
// --------------------------------------------------------------------------------------------------------------------

namespace WpInsights.Core
{
    using System;
    using System.Globalization;
    using System.IO;
    using System.Net;
    using System.Security.Cryptography;
    using System.Text;
    using System.Web;
    using System.Xml;

    using Microsoft.Win32;

    /// <summary>The http request extensions.</summary>
    public static class RequestExtensions
    {
        /// <summary>The authorization header delimiter.</summary>
        private const string AuthorizationHeaderDelimiter = ";";

        /// <summary>The version header (connector version).</summary>
        private const string VersionHeader = "x-ms-version";

        /// <summary>The request content header.</summary>
        private const string ContentHeader = "x-ms-SHA256_Content";

        /// <summary>The standard date header.</summary>
        private const string DateHeader = "x-ms-Date";

        /// <summary>The standard authorization header.</summary>
        public const string AuthorizationHeader = "Authorization";

        /// <summary>The latest connector version.</summary>
        private const string CurrentConnectorVersion = "April, 2013";

        /// <summary> Where account name and keys are stored - path </summary>
        private const string StorageKeyRegistryPath = @"SYSTEM\CurrentControlSet\Services\HealthService\Parameters\SecureStorageManager";

        /// <summary> Where account name and keys are stored - value </summary>
        private const string StorageKeyRegistryValue = @"AccountSharedKey";

        /// <summary>Update request to include authentication information</summary>
        /// <param name="request">The request.</param>
        /// <param name="content">Request content (it is not readable from the request)</param>
        public static void Sign(this HttpWebRequest request, string content)
        {
            if (request == null)
            {
                throw new ArgumentNullException("request");
            }

            string accountId;
            string accountKey;

            ReadAccountIdAndKey(out accountId, out accountKey);

            Sign(request, accountId, accountKey, content);
        }

        /// <summary>Update request to include authentication information</summary>
        /// <param name="request">The request.</param>
        /// <param name="content">Request content (it is not readable from the request)</param>
        public static void Sign(this HttpWebRequest request, byte[] content)
        {
            if (request == null)
            {
                throw new ArgumentNullException("request");
            }

            string accountId;
            string accountKey;

            ReadAccountIdAndKey(out accountId, out accountKey);

            Sign(request, accountId, accountKey, content);
        }

        /// <summary>Update request to include authentication information</summary>
        /// <param name="request">The request.</param>
        /// <param name="accountId">The account Id: GUID</param>
        /// <param name="accountKey">The account Key: Base64 string</param>
        /// <param name="content">Request content (it is not readable from the request)</param>
        public static void Sign(this HttpWebRequest request, string accountId, string accountKey, string content)
        {
            if (content == null)
            {
                // empty string is acceptable
                throw new ArgumentNullException("content");
            }

            byte[] contentBytes = Encoding.UTF8.GetBytes(content);
            Sign(request, accountId, accountKey, contentBytes);
        }


        /// <summary>Update request to include authentication information</summary>
        /// <param name="request">The request.</param>
        /// <param name="accountId">The account Id: GUID</param>
        /// <param name="accountKey">The account Key: Base64 string</param>
        /// <param name="content">Request content (it is not readable from the request)</param>
        public static void Sign(this HttpWebRequest request, string accountId, string accountKey, byte[] content)
        {
            //// x-ms-version: April, 2013
            //// x-ms-SHA256_Content: TeKFhRNfGx1M1ERJpWWJSf5fsrrS1sin/8GDKUe2G/U=
            //// Date: Date in GMT format
            //// Authorization: accountId;nhc15hNfbPOAZ6L3jHU2SWrIz6z2BZnuS+IpMcAzMrY=
            
            if (request == null)
            {
                throw new ArgumentNullException("request");
            }

            if (accountKey == null)
            {
                throw new ArgumentNullException("accountKey");
            }

            if (accountId == null)
            {
                throw new ArgumentNullException("accountId");
            }

            //// Sign content with key
            string requestTime = XmlConvert.ToString(DateTime.Now, XmlDateTimeSerializationMode.Utc); // Not Utc.New as zone will be part of the string format
            string contentHash = ComputeHash(content);
            string authHeader = CreateAuthorizationHeader(accountId, accountKey, content, requestTime);

            // Add headers
            request.Headers.Add(VersionHeader, CurrentConnectorVersion);
            request.Headers.Add(DateHeader, requestTime);
            request.Headers.Add(ContentHeader, contentHash);
            request.Headers.Add(AuthorizationHeader, authHeader);
        }

        /// <summary>Get account id.</summary>
        /// <param name="request">The request.</param>
        /// <returns>Account id</returns>
        public static string GetAccountId(this HttpRequest request)
        {
            if (request == null)
            {
                throw new ArgumentNullException("request");
            }

            string result = string.Empty;
            string header = request.Headers[AuthorizationHeader];
            if (!string.IsNullOrEmpty(header))
            {
                result = header.GetAccountId();
            }

            return result;
        }

        public static string GetAccountId(this string header)
        {
            string result = string.Empty;
            if (!string.IsNullOrEmpty(header))
            {
                int ind = header.IndexOf(AuthorizationHeaderDelimiter, StringComparison.InvariantCulture);

                if (ind > 0)
                {
                    result = header.Substring(0, ind);
                }
            }

            return result;
        }

        /// <summary>Get request signature</summary>
        /// <param name="request">The request.</param>
        /// <returns>Account id</returns>
        public static string GetRequestSignature(this HttpRequest request)
        {
            if (request == null)
            {
                throw new ArgumentNullException("request");
            }

            string header = request.Headers[AuthorizationHeader];

            string result = GetRequestSignature(header);
            
            return result;
        }

        /// <summary>Get request signature</summary>
        /// <returns>Account id</returns>
        public static string GetRequestSignature(string header)
        {
            string result = string.Empty;

            if (!string.IsNullOrEmpty(header))
            {
                int ind = header.IndexOf(AuthorizationHeaderDelimiter, StringComparison.InvariantCulture);

                if (ind > 0)
                {
                    result = header.Substring(ind + 1);
                }
            }

            return result;
        }

        /// <summary>Get request date.</summary>
        /// <param name="request">The request.</param>
        /// <returns>The request date</returns>
        public static DateTime GetRequestDate(this HttpRequest request)
        {
            if (request == null)
            {
                throw new ArgumentNullException("request");
            }

            DateTime result;

            string header = request.Headers[DateHeader];

            DateTime.TryParse(header, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal, out result);

            return result;
        }

        /// <summary>Check if request has authentication headers</summary>
        /// <param name="request">The http request.</param>
        /// <returns>True if request has authentication headers</returns>
        public static bool HasAuthHeaders(this HttpRequest request)
        {
            if (request == null)
            {
                throw new ArgumentNullException("request");
            }

            var hasDate = request.Headers[DateHeader] != null;
            var hasVersion = request.Headers[VersionHeader] != null;
            var hasContent = request.Headers[ContentHeader] != null;
            var hasAuthorization = request.Headers[AuthorizationHeader] != null;
            
            bool result = hasDate & hasVersion & hasContent & hasAuthorization;
            return result;
        }

        /// <summary>Get authentication header.</summary>
        /// <param name="request">The request.</param>
        /// <returns>The authentication header</returns>
        public static string GetAuthenticationHeader(this HttpRequest request)
        {
            if (request == null)
            {
                throw new ArgumentNullException("request");
            }

            return request.Headers[AuthorizationHeader];
        }

        /// <summary>Create authorization header.</summary>
        /// <param name="request">The request.</param>
        /// <param name="accountId">The account id.</param>
        /// <param name="key">The key.</param>
        /// <returns>Create authentication header</returns>
        public static string CreateAuthorizationHeader(this HttpRequest request, string accountId, string key)
        {
            if (request == null)
            {
                throw new ArgumentNullException("request");
            }

            string requestTimeString = request.Headers[DateHeader];

            byte[] contentBytes = GetRequestContent(request.InputStream);
            
            string authHeader = CreateAuthorizationHeader(accountId, key, contentBytes, requestTimeString);

            return authHeader;
        }

        /// <summary>
        /// Read account id and account key from the SecureStorage
        /// </summary>
        /// <param name="accountId">Account id</param>
        /// <param name="accountKey">Account key</param>
        private static void ReadAccountIdAndKey(out string accountId, out string accountKey)
        {
            byte[] sharedKey = null;
            const int EntropyLength = 1024;

            using (RegistryKey storageKey = Registry.LocalMachine.OpenSubKey(StorageKeyRegistryPath))
            {
                if (storageKey != null)
                {
                    sharedKey = storageKey.GetValue(StorageKeyRegistryValue, null) as byte[];
                }
            }

            if (sharedKey == null)
            {
                throw new ApplicationException("Account name and key were not found " + StorageKeyRegistryPath);
            }

            if (sharedKey.Length > EntropyLength)
            {
                byte[] encryptedData = new byte[sharedKey.Length - EntropyLength];
                byte[] entropyData = new byte[EntropyLength];
                Array.Copy(sharedKey, 0, encryptedData, 0, encryptedData.Length);
                Array.Copy(sharedKey, encryptedData.Length, entropyData, 0, EntropyLength);

                byte[] decryptedData = ProtectedData.Unprotect(encryptedData, entropyData, DataProtectionScope.LocalMachine);
                char[] decryptedChars = Encoding.Unicode.GetChars(decryptedData);
                int keyIndex = 0;
                for (int index = 0; index < decryptedChars.Length; index++)
                {
                    if (decryptedChars[index] == '\0')
                    {
                        keyIndex = index;
                        break;
                    }
                }

                accountId = new string(decryptedChars, 0, keyIndex);
                accountKey = new string(decryptedChars, keyIndex + 1, decryptedChars.Length - keyIndex - 2);

                // Try to convert and throw if it is not a valid string
                Convert.FromBase64String(accountKey);
            }
            else
            {
                throw new ApplicationException("Account key has unexpected length " + sharedKey.Length);
            }
        }

        /// <summary>Return the request content.</summary>
        /// <param name="stream">The request stream.</param>
        /// <returns>The request content</returns>
        private static byte[] GetRequestContent(Stream stream)
        {
            if (stream == null)
            {
                throw new ArgumentNullException("stream");
            }

            byte[] buffer = new byte[4 * 1024];
            byte[] result;

            using (var ms = new MemoryStream())
            {
                int read;
                while ((read = stream.Read(buffer, 0, buffer.Length)) > 0)
                {
                    ms.Write(buffer, 0, read);
                }

                result = ms.ToArray();
            }

            stream.Position = 0; // Need to rewind the stream, or we won't be able to process the data.
            
            return result;
        }

        /// <summary>Create authorization header.</summary>
        /// <param name="accountId">The account id.</param>
        /// <param name="key">The key.</param>
        /// <param name="content">The content.</param>
        /// <param name="date">The date.</param>
        /// <returns>the authentication header</returns>
        private static string CreateAuthorizationHeader(string accountId, string key, byte[] content, string date)
        {
            //// Authorization: accountId; nhc15hNfbPOAZ6L3jHU2SWrIz6z2BZnuS+IpMcAzMrY=
            //// *** IMPORTANT ***
            //// The code here needs to exactly match CKeyAuthenticationProvider::SetRequestAuthenticationContext()
            //// in private\product\HealthService\ServiceCore\Managers\Connector\Source\Endpoint.cpp in agent code;
            //// otherwise the authentication will fail.

            // The key is encoded in Base64.
            byte[] keyBytes = Convert.FromBase64String(key);
            var hmacKey = new HMACSHA256(keyBytes);

            string contentHash = ComputeHash(content);
            
            string signatureBuilder = string.Concat(date, "\n", contentHash, "\n");
            byte[] signatureBytes = Encoding.Unicode.GetBytes(signatureBuilder);
            byte[] signatureHash = hmacKey.ComputeHash(signatureBytes);

            string signature = Convert.ToBase64String(signatureHash);

            string result = string.Concat(accountId, AuthorizationHeaderDelimiter, " ", signature);
            return result;
        }

        /// <summary>Compute hash.</summary>
        /// <param name="contentBytes">The content.</param>
        /// <returns>Hashed content</returns>
        private static string ComputeHash(byte[] contentBytes)
        {
            byte[] shaHash;

            using (var sha = new SHA256CryptoServiceProvider())
            {
                shaHash = sha.ComputeHash(contentBytes);
            }

            string result = Convert.ToBase64String(shaHash);

            return result;
        }
    }
}
